import ts from 'typescript';
import path from 'path';
import fs from 'fs';
import { createMatchPath, MatchPath } from 'tsconfig-paths';
import { formatCompilerOptions } from './options';
import LanguageServiceHost from './LanguageServiceHost';
import { getAllDependencies, replaceString, logWarning, findNPMDependencies, encodeFilePath } from './utils';

export interface Options {
  extensions: string[];
}

class Engine {
  private _cwd: string;
  private _serviceHost: LanguageServiceHost;
  private _service: ts.LanguageService;
  private _options: Options;
  private _pathsMatch: MatchPath;
  private _tempSet: Set<string> = new Set(); // fix circle import
  private _fileNameMap: Map<string, string> = new Map();

  constructor(cwd: string, opts: Options) {
    this._cwd = cwd;
    this._options = opts;
    const configPath = path.resolve(cwd, 'tsconfig.json');
    const { error, config } = ts.readConfigFile(configPath, (url) => fs.readFileSync(url, 'utf-8'));
    if (error) {
      throw new Error(`Engine: read tsconfig.json failed ${error.messageText}`);
    }
    const { options, errors } = ts.convertCompilerOptionsFromJson(config.compilerOptions, cwd);
    if (errors.length !== 0) {
      let errStr = '';
      for (let i = 0; i < errors.length; i += 1) {
        errStr += `${errors[i].messageText}\n`;
      }
      throw new Error(errStr);
    }
    const compilerOptions = formatCompilerOptions(options);
    this._serviceHost = new LanguageServiceHost(compilerOptions, cwd);
    this._service = ts.createLanguageService(this._serviceHost, ts.createDocumentRegistry());
    this._pathsMatch = createMatchPath(compilerOptions.baseUrl || this._cwd, compilerOptions.paths || {});
  }

  private _getDeclareFile(fileName: string) {
    const code = fs.readFileSync(fileName, 'utf-8');
    this._serviceHost.setScriptSnapshot(fileName, code);
    const { outputFiles } = this._service.getEmitOutput(fileName, true, true);
    return outputFiles[0]?.text;
  }

  generate(input: string, output: string, fileName: string) {
    this._tempSet.clear();
    this._fileNameMap.clear();
    this._loop(input, output, fileName);
  }

  private _checkExtensions(input: string) {
    for (let i = 0; i < this._options.extensions.length; i += 1) {
      if (input.endsWith(this._options.extensions[i])) {
        return true;
      }
    }
    return false;
  }

  private _loop(input: string, output: string, fileName: string) {
    if (!this._checkExtensions(input)) return;
    let declareText = this._getDeclareFile(input);

    const dependencies = getAllDependencies('output.d.ts', declareText);

    let diff = 0;

    for (let i = 0; i < dependencies.length; i += 1) {
      let fileUrl = this._resolvePath(dependencies[i].source, input);
      if (!fileUrl) {
        logWarning(`generate: can not load dependencies ${dependencies[i].source} on ${input}`);
      } else if (!/node_modules/.test(fileUrl))  {
        const key = input + '-|-' + fileUrl;
        const encodedUrl = encodeFilePath(this._fileNameMap, fileUrl);
        const itemFileName = encodedUrl + '.d.ts';
        const replacer = ` './${encodedUrl}'`;
        // replace 了之后 import 语句的position 即， start, end 发生了改变。所以要记录变化值
        declareText = replaceString(declareText, dependencies[i].start + diff, dependencies[i].end + diff, replacer);
        diff += replacer.length - (dependencies[i].end - dependencies[i].start);
        if (!this._tempSet.has(key)) {
          this._tempSet.add(key);
          this._loop(fileUrl, output, itemFileName)
        }
      }
    }

    const destPath = path.resolve(output, fileName);
    fs.writeFileSync(destPath, declareText);
  }

  private _resolvePath(source: string, importer: string) {
    const fileUrl = this._findPath(source, importer);
    if (fileUrl) {
      return path.resolve(fileUrl);
    }
    return null;
  }

  private _findPath(source: string, importer: string) {
    let url = this._pathsMatch(source, undefined, undefined, this._options.extensions);
    if (url && fs.existsSync(url)) {
      return url;
    }
    // find on typings
    for (let i = 0; i < this._options.extensions.length; i += 1) {
      const itemUrl = url + this._options.extensions[i];
      if (fs.existsSync(itemUrl)) {
        return itemUrl;
      }
    }
    const dirname = path.dirname(importer);
    url = path.resolve(dirname, source);
    if (url && fs.existsSync(url)) {
      return url;
    }
    // find on file
    for (let i = 0; i < this._options.extensions.length; i += 1) {
      const itemUrl = url + this._options.extensions[i];
      if (fs.existsSync(itemUrl)) {
        return itemUrl;
      }
    }
    // find on node_modules
    return findNPMDependencies(source, importer);
  }
}

export default Engine;
